package dungeons;

import java.util.HashMap;
import java.util.HashSet;

import squares.Direction;
import squares.IllegalSquareException;
import squares.Square;
import be.kuleuven.cs.som.annotate.Model;
import borders.Border;
import borders.Door;
import borders.Open;
import borders.Wall;
/**
 * An abstract class as a special kind of dungeon that represents a shaft or a plateau and can be added to a
 * composite dungeon
 * @invar 
 * @param <E>
 * 			this parameter represents the kind of square this dungeon contains.
 * @version 1.0
 * @author 	Gilles Groven & Jochen Tombal
 */
public abstract class SingleDungeon<E extends Square> extends Dungeon<E>{

	/**
	 * An interwoven HashMap with the coordinates as a key and the extended squares as object
	 */
	private HashMap<Long,HashMap<Long,HashMap<Long,E>>> dimensions;
	/**
	 * A Hashset registering the different squares in a dungeon so there can't be any dublicate
	 */
	private HashSet<E> squares;
	
	
	/**
	*a dungeon class that represents a plateau or a shaft dungeon.
	*
	*/
	protected SingleDungeon(long x, long y, long z)
			throws IllegalArgumentException {
		super(x, y, z);
		dimensions = new HashMap<Long,HashMap<Long,HashMap<Long,E>>>();
		squares = new HashSet<E>();
	}
	/**
	 * 
	 * @return	the HashMap dimensions which contains all the squares related to its coordinates of this dungeon.
	 */

	protected HashMap<Long, HashMap<Long, HashMap<Long, E>>> getDimensions() {
		return dimensions;
	}
	/**
	 * 
	 * @return the squares HashSet containing the squares of this dungeon
	 */
	protected HashSet<E> getSquares() {
		return squares;
	}
	/**
	 * adding a square to the HashSet squares of this dungeon
	 * 
	 * @param square
	 * 			the square that is to be added to the HashSet
	 */
	protected void addToSquaresList(E square)
	{
		squares.add(square);
	}
	
	/**
	 * checks if adding a new slippery square will result in more then 20% squares with slippery floor in the dungeon
	 * @return
	 * 		true if adding a new slippery square will result in more then 20% slippery squares.
	 * 		|int i = 1;
	 *		|for( Square square: squares)
	 *	 	|	if(square.getSlipperyFloor())
	 *		|		i++;	
	 *		|if(i/(squares.size()+1) > 0.2 )
	 *		|	then result == true;
	 */
	@SuppressWarnings("unchecked")
	@Model
	protected boolean toManySlippery()
	{
		int i = 1;
		for( Square square: (HashSet<Square>)squares)
		{
			if(square.getSlipperyFloor())
				i++;	
		}
		if(i/(squares.size()+1) > 0.2 )
			return true;
		else
			return false;
	}
	
	/**
	 * adds a new square to the hashmap of the dungeon if it has unique coordinates 
	 * 
	 * @param 	square
	 * 			the square that is to be added
	 * @param x
	 * 			the x value of the square its coordinate
	 * @param y
	 * 			the y value of the square its coordinate
	 * @param z
	 * 			the z value of the square its coordinate
	 * @post
	 * 			the square is added to the hashmap
	 * 			|dimensions.get(x).get(y).get(z) == square
	 */
	
	protected void addSquareToHashMap( E square, long x, long y, long z)
	{
		
		if(dimensions.get(x)== null)
		{
			HashMap<Long,E> zMap = new HashMap<Long, E>();
			zMap.put(z, square);
			HashMap<Long, HashMap<Long, E>> yMap = new HashMap<Long,HashMap<Long,E> >();
			yMap.put(y,zMap);
			dimensions.put(x, yMap);
		}
		else if(dimensions.get(x).get(y) == null)
		{
			HashMap<Long,E> zMap = new HashMap<Long,E>();
			zMap.put(z,square);
			dimensions.get(x).put(y, zMap);
		}
		else if(dimensions.get(x).get(y).get(z) == null)
		{
			dimensions.get(x).get(y).put(z,square);
		}
	}
	
	
	/**
	 * The collection of squares that is enclosed by walls or closed doors.
	 * 
	 * @param x
	 * 			the x coordinate from which the enclosed space is to be found
	 * @param y
	 * 			the y coordinate from which the enclosed space is to be found
	 * @param z
	 * 			the z coordinate from which the enclosed space is to be found
	 * @throws	IllegalArgumentException
	 * 			if the given coordinates are invalid, all equal or biger then their max value
	 * 				or don't match with a existing square
	 * 			|(x==y && y==z) || x<getMaxX() || y<getMaxY() || z<getMaxZ() ||
	 * 			|getSquareAt(x,y,z) == null
	 * TODO zie dungeon findSpace(square) return fout
	 * @return
	 * 			a hashmap containing two different maps : a "squares" key with all the squares in the space and a "rocks" key with all the rocks surrounding the space.
	 * 			|for(Square squareOther: squares)
	 * 			|{ if(isConnected(squareOther,square))
	 * 			|	then result.get("squares").contains(squareOther)
	 * 
	 */
	
	public HashMap<String,HashSet<Square>> findSpace(long x, long y, long z) throws IllegalArgumentException
	{
		if((x==y && y==z) || x<getMaxX() || y<getMaxY() || z<getMaxZ())
			throw new IllegalArgumentException("invalid coordinate");
		if(getSquareAt(x,y,z) == null)
			throw new IllegalArgumentException("invalid coordinate");
		Square square = (Square) getSquareAt(x,y,z);
		return findSpace(square);
	}
	
	/**
	 * merges a square his borders with its potential neighbors. 
	 * 
	 * @param square
	 * 			the square which borders has to be merged with its potential neighbors
	 * @param x
	 * 			the x value of the square its coordinate
	 * @param y
	 * 			the y value of the square its coordinate
	 * @param z
	 * 			the z value of the square its coordinate
	 * @throws IllegalArgumentException
	 * 			throws this exception if invalid input is given for the coordinates
	 * 			| x>getMaxX || y> getMaxY || z> getMaxZ || x<0 || y<0 || z<0 || (x==y && y==z)
	 * @throws IllegalSquareException 
	 * 			throws this exception if the given square is null
	 * 			| square == null;
	 * @post	the square has shared borders with the squares located at positions around it.
	 * 			|  square.new.getBorderAt(Direction.CEILING).equals(getSquareAt(x,y,z+1).getBorderAt(Direction.FLOOR) || square.new.getBorderAt(Direction.CEILING).getOtherAttachedSquare(square) == null
	 * 			|  square.new.getBorderAt(Direction.FLOOR).equals(getSquareAt(x,y,z-1).getBorderAt(Direction.CEILING) || square.new.getBorderAt(Direction.FLOOR).getOtherAttachedSquare(square) == null
	 * 			|  square.new.getBorderAt(Direction.NORTH).equals(getSquareAt(x,y+1,z).getBorderAt(Direction.SOUTH) || square.new.getBorderAt(Direction.NORTH).getOtherAttachedSquare(square) == null	 
	 * 			|  square.new.getBorderAt(Direction.SOUTH).equals(getSquareAt(x,y-1,z).getBorderAt(Direction.NORTH) || square.new.getBorderAt(Direction.SOUTH).getOtherAttachedSquare(square) == null
	 * 			|  square.new.getBorderAt(Direction.EAST).equals(getSquareAt(x+1,y,z).getBorderAt(Direction.WEST) || square.new.getBorderAt(Direction.EAST).getOtherAttachedSquare(square) == null
	 * 			|  square.new.getBorderAt(Direction.WEST).equals(getSquareAt(x-1,y,z).getBorderAt(Direction.EAST) || square.new.getBorderAt(Direction.WEST).getOtherAttachedSquare(square) == null	 
	 */
	@Override
	protected void mergeBorders(E square, long x,long y, long z) throws IllegalArgumentException, IllegalSquareException
	{
		if(square == null)
			throw new IllegalSquareException("invalid input");
		else if(x==y && y==z)
			throw new IllegalArgumentException("the values are the same.");
		else if(x>getMaxX() || y>getMaxY() || z>getMaxZ() || x<0 || y<0 || z<0)
			throw new IllegalArgumentException("the values exceed the maximum dimensions or are below zero");
		if(getSquareAt(x,y,z+1) != null)
		{
			Border border = getMostImportantBorder(((Square) square).getBorderAt(Direction.CEILING),((Square) getSquareAt(x,y,z+1)).getBorderAt(Direction.CEILING));
			((Square) square).changeBorderAt(Direction.CEILING, border);
			((Square) getSquareAt(x,y,z+1)).changeBorderAt(Direction.FLOOR, border);
			border.setAttachedSquareTo(Direction.CEILING, (Square) square);
			border.setAttachedSquareTo(Direction.FLOOR, (Square) getSquareAt(x,y,z+1));
		}
		if(getSquareAt(x,y,z-1) != null)
		{
			Border border = getMostImportantBorder(((Square) square).getBorderAt(Direction.FLOOR),((Square) getSquareAt(x,y,z-1)).getBorderAt(Direction.FLOOR));
			((Square) square).changeBorderAt(Direction.FLOOR, border);
			((Square) getSquareAt(x,y,z-1)).changeBorderAt(Direction.FLOOR, border);
			border.setAttachedSquareTo(Direction.FLOOR, (Square) square);
			border.setAttachedSquareTo(Direction.CEILING, (Square) getSquareAt(x,y,z-1));
		}
		if(getSquareAt(x,y+1,z) != null)
		{
			Border border = getMostImportantBorder(((Square) square).getBorderAt(Direction.NORTH),((Square) getSquareAt(x,y+1,z)).getBorderAt(Direction.SOUTH));
			((Square) square).changeBorderAt(Direction.NORTH, border);
			((Square) getSquareAt(x,y+1,z)).changeBorderAt(Direction.SOUTH, border);
			border.setAttachedSquareTo(Direction.NORTH, (Square) square);
			border.setAttachedSquareTo(Direction.SOUTH, (Square) getSquareAt(x,y+1,z));

		}
		if(getSquareAt(x,y-1,z) != null)
		{
			Border border = getMostImportantBorder(((Square) square).getBorderAt(Direction.SOUTH),((Square) getSquareAt(x,y-1,z)).getBorderAt(Direction.NORTH));
			((Square) square).changeBorderAt(Direction.SOUTH, border);
			((Square) getSquareAt(x,y-1,z)).changeBorderAt(Direction.NORTH, border);
			border.setAttachedSquareTo(Direction.SOUTH, (Square) square);
			border.setAttachedSquareTo(Direction.NORTH, (Square) getSquareAt(x,y-1,z));
		}
		if(getSquareAt(x+1,y,z) != null)
		{
			Border border = getMostImportantBorder(((Square) square).getBorderAt(Direction.EAST),((Square) getSquareAt(x+1,y,z)).getBorderAt(Direction.WEST));
			((Square) square).changeBorderAt(Direction.EAST, border);
			((Square) getSquareAt(x+1,y,z)).changeBorderAt(Direction.WEST, border);
			border.setAttachedSquareTo(Direction.EAST, (Square) square);
			border.setAttachedSquareTo(Direction.WEST, (Square) getSquareAt(x+1,y,z));
		}
		if(getSquareAt(x-1,y,z) != null)
		{
			Border border = getMostImportantBorder(((Square) square).getBorderAt(Direction.WEST),((Square) getSquareAt(x-1,y,z)).getBorderAt(Direction.EAST));
			((Square) square).changeBorderAt(Direction.WEST, border);
			((Square) getSquareAt(x-1,y,z)).changeBorderAt(Direction.EAST, border);
			border.setAttachedSquareTo(Direction.WEST, (Square) square);
			border.setAttachedSquareTo(Direction.EAST, (Square) getSquareAt(x-1,y,z));
		}
	}
	
	/**
	 * checks if there is already a square present on the given coordinate.
	 */
	@Override
	protected boolean duplicateCoordinate(long x, long y, long z) {
		if(dimensions.get(x) != null)
		{
			if(dimensions.get(x).get(y)  !=null)
			{
				if(dimensions.get(x).get(y).get(z) !=null)
				{
					return true;
				}
			}
		}
		return false;
	}
	
	/**
	 * a method to find a square on a specific location in the dungeon
	 */
	@Override
	public E getSquareAt(long x, long y, long z)
	{
		if(dimensions.get(x) == null)
		{
			return null;
		}
		else if(dimensions.get(x).get(y) == null)
		{
			return null;
		}
		else
			return dimensions.get(x).get(y).get(z);
	}
	
	
	/**
	 * @return
	 * 		a copy of the hashset containing all the squares in the dungeon
	 */
	@Override 
	@SuppressWarnings("unchecked")
	@Model
	protected HashSet<E> getAllSquares()
	{
		return (HashSet<E>) squares.clone();
	}
	
	/**
	 * deletes a square completely from the dungeon.
	 * @param x
	 * 		the x coordinate of the square you want to remove
	 * @param y
	 * 		the y coordinate of the square you want to remove
	 * @param z
	 * 		the z coordinate of the square you want to remove
	 * @post
	 * 		the square is removed from the dungeon and there is no reference left to the square
	 * 		| new.getSquareAt(x,y,z) == null
	 * 		| for each square in new.getAllSquares()
	 * 		|	for all directions
	 * 		|	new.border.getAttachedSquare1() != this.getSquareAt(x,y,z)
	 * 		|   new.border.getAttachedSquare2() != this.getSquateAt(x,y,z)
	 * 		|getSquares().contains(this.getSquareAt(x,y,z)) == false
	 */
	@SuppressWarnings("unchecked")
	public void deleteSquare(long x, long y, long z) {
		Square square = (Square) this.getSquareAt(x,y,z);
		
		if(square != null)
		{
			for(Direction direction: Direction.values())
			{
				if(square.getBorderAt(direction).hasOnlyOneSquareAttached())
				{				
				}
				else
				{	
					Square other = square.getBorderAt(direction).getOtherAttachedSquare(square);
					if(square.getBorderAt(direction) instanceof Wall)
						square.changeBorderAt(direction, new Wall(square,((Wall)square.getBorderAt(direction)).getSlipperyOfWall()));
					else if(square.getBorderAt(direction) instanceof Door)
						square.changeBorderAt(direction, new Door(square,((Door)square.getBorderAt(direction)).getIsOpen()));
					else if(square.getBorderAt(direction) instanceof Open)
						square.changeBorderAt(direction, new Open(square));
					else
						square.changeBorderAt(direction, new Open(square));
					other.getBorderAt(Square.getOppositeDirection(direction)).deleteAttachedSquare(direction, square);	
				}
			}
			for(Direction direction: Direction.values())
				square.getBorderAt(direction).detachSquare (square, x, y,  z, this);
			Square.deleteSquare(square,x,y,z,(Dungeon<Square>)this);
			
			if (getDimensions().get(x).isEmpty())
				getDimensions().remove(x);
			else if (getDimensions().get(x).get(y).isEmpty())
				getDimensions().get(x).remove(y);
			else
				getDimensions().get(x).get(y).remove(z);
			//TODO zijn we dit niet vergeten?
			getSquares().remove(square);
			
		}
		
	}
	
	/**
	 * deletes a dungeon completely with all its squares.
	 * 
	 *@effect the dungeon is completely removed and so its squares
	 *		|for(long x: this.dimensions.keySet())
	 *		|	for(long y:this.dimensions.get(x).keySet())
	 *		|		for(long z:this.dimensions.get(x).get(y).keySet())
	 *		|			if(this.dimensions.get(x).get(y).get(z) instanceof Square)
	 *		|				Square.deleteSquare((Square)this.dimensions.get(x).get(y).get(z), x, y, z,(SingleDungeon<Square>) this)
	 *		|new.dimensions.isEmpty()
	 *		|new.isTerminated() == true
	 *		|new.squares.isEmpty()
	 *@post if this dungeon has a rootDungeon, it is part of a composite dungeon, then its composite dungeon doesn't contain
	 *		this dungeon.
	 *		|if(!this.getRootDungeon() == null)
	 *		|	then  this.getRootDungeon().getCompositeDungeonhavingAsSubDungeon(this) == null
	 *		|		  && new.getRootDungeon() == null
	 */
	
	@Override
	@SuppressWarnings("unchecked")
	public void Terminate ()
	{
		if(this.getRootDungeon() == null)
		{
			for(long x: this.dimensions.keySet())
			{
				for(long y:this.dimensions.get(x).keySet())
				{
					for(long z:this.dimensions.get(x).get(y).keySet())
					{
					if(	this.dimensions.get(x).get(y).get(z) instanceof Square)
					{
						Square.deleteSquare((Square)this.dimensions.get(x).get(y).get(z), x, y, z,(SingleDungeon<Square>) this);
					}
					}
				}
			}
		}
		else
		{
			CompositeDungeon<E> oneUpDungeon = this.getRootDungeon().getCompositeDungeonhavingAsSubDungeon(this);
			long[] coords = oneUpDungeon.getRootLocationOfDungeon(this);
			for(long x: this.dimensions.keySet())
			{
				for(long y:this.dimensions.get(x).keySet())
				{
					for(long z:this.dimensions.get(x).get(y).keySet())
					{
					if(	this.dimensions.get(x).get(y).get(z) instanceof Square)
					{
						Square.deleteSquare((Square)this.dimensions.get(x).get(y).get(z), x, y, z,(SingleDungeon<Square>) this);
					}
					}
				}
			}
			oneUpDungeon.getSubDungeons().remove(this);
			oneUpDungeon.getDimensions().get(coords[0]).get(coords[1]).remove(coords[2]);
			if(oneUpDungeon.getDimensions().get(coords[0]).get(coords[1]).isEmpty())
			{
				oneUpDungeon.getDimensions().get(coords[0]).remove(coords[1]);
				if(oneUpDungeon.getDimensions().get(coords[0]).isEmpty())
					oneUpDungeon.getDimensions().remove(coords[0]);
			}
			this.setRootDungeon(null);
		}
		this.dimensions.clear();
		this.isTerminated();
		this.squares.clear();
	}
	
	
	
	
	
}
